📦 S3 Bucket Browser (Interactive)

Bucket: jsmith-output
Total Files: 23,620
⚠️ Warning: Delete operations are permanent and cannot be undone!

📊 Compare Folders

Folder 1:
Not selected
Folder 2:
Not selected

Comparison Results

folder1: null, folder2: null }; function toggleFolder(element) { element.classList.toggle('expanded'); const nested = element.parentElement.querySelector('.nested'); if (nested) { nested.classList.toggle('active'); } } function expandAll() { document.querySelectorAll('.nested').forEach(el => { el.classList.add('active'); }); document.querySelectorAll('.folder-toggle').forEach(el => { el.classList.add('expanded'); }); } function collapseAll() { document.querySelectorAll('.nested').forEach(el => { el.classList.remove('active'); }); document.querySelectorAll('.folder-toggle').forEach(el => { el.classList.remove('expanded'); }); } function expandLevel(level) { collapseAll(); function expandToLevel(element, depth) { if (depth >= level) return; const nested = element.querySelector(':scope > .nested'); const toggle = element.querySelector(':scope > .folder-toggle'); if (nested && toggle) { nested.classList.add('active'); toggle.classList.add('expanded'); const childFolders = nested.querySelectorAll(':scope > .folder'); childFolders.forEach(child => { expandToLevel(child, depth + 1); }); } } document.querySelectorAll('#tree-root > .folder').forEach(folder => { expandToLevel(folder, 0); }); } function searchTree(query) { query = query.toLowerCase().trim(); if (!query) { document.querySelectorAll('li').forEach(el => { el.style.display = ''; }); collapseAll(); return; } document.querySelectorAll('li').forEach(el => { el.style.display = 'none'; }); document.querySelectorAll('li').forEach(el => { const text = el.textContent.toLowerCase(); if (text.includes(query)) { let current = el; while (current) { current.style.display = ''; const parentUl = current.parentElement; if (parentUl && parentUl.classList.contains('nested')) { parentUl.classList.add('active'); const parentLi = parentUl.parentElement; if (parentLi) { const toggle = parentLi.querySelector(':scope > .folder-toggle'); if (toggle) { toggle.classList.add('expanded'); } } } current = current.parentElement?.closest('li'); } } }); } function showNotification(message, isSuccess) { const notification = document.createElement('div'); notification.className = `notification ${isSuccess ? 'success' : 'error'}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.remove(); }, 3000); } async function deleteItem(path, isFolder, button) { const itemType = isFolder ? 'folder' : 'file'; const confirmMessage = isFolder ? `Are you sure you want to delete the folder "${path}" and ALL its contents?\n\nThis action cannot be undone!` : `Are you sure you want to delete "${path}"?\n\nThis action cannot be undone!`; if (!confirm(confirmMessage)) { return; } const listItem = button.closest('li'); listItem.classList.add('deleting'); try { const response = await fetch('/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: path, isFolder: isFolder }) }); const result = await response.json(); if (result.success) { showNotification(result.message, true); listItem.remove(); } else { showNotification(`Error: ${result.message}`, false); listItem.classList.remove('deleting'); } } catch (error) { showNotification(`Error: ${error.message}`, false); listItem.classList.remove('deleting'); } } function selectForCompare(path, button) { const comparePanel = document.getElementById('comparePanel'); comparePanel.classList.add('active'); // Toggle selection if (button.classList.contains('selected')) { // Deselect button.classList.remove('selected'); if (selectedFolders.folder1 === path) { selectedFolders.folder1 = null; document.getElementById('folder1Path').textContent = 'Not selected'; } else if (selectedFolders.folder2 === path) { selectedFolders.folder2 = null; document.getElementById('folder2Path').textContent = 'Not selected'; } } else { // Select if (!selectedFolders.folder1) { selectedFolders.folder1 = path; document.getElementById('folder1Path').textContent = path; button.classList.add('selected'); } else if (!selectedFolders.folder2) { selectedFolders.folder2 = path; document.getElementById('folder2Path').textContent = path; button.classList.add('selected'); } else { showNotification('Already selected 2 folders. Deselect one first.', false); return; } } // Enable compare button if both selected const compareBtn = document.getElementById('compareBtn'); compareBtn.disabled = !(selectedFolders.folder1 && selectedFolders.folder2); } function cancelCompare() { // Clear selections document.querySelectorAll('.compare-btn.selected').forEach(btn => { btn.classList.remove('selected'); }); selectedFolders.folder1 = null; selectedFolders.folder2 = null; document.getElementById('folder1Path').textContent = 'Not selected'; document.getElementById('folder2Path').textContent = 'Not selected'; document.getElementById('comparePanel').classList.remove('active'); document.getElementById('compareBtn').disabled = true; } async function compareSelectedFolders() { if (!selectedFolders.folder1 || !selectedFolders.folder2) { showNotification('Please select two folders to compare', false); return; } showNotification('Comparing folders... This may take a moment.', true); try { const response = await fetch('/compare', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folder1: selectedFolders.folder1, folder2: selectedFolders.folder2 }) }); const result = await response.json(); if (result.success) { displayCompareResults(result); } else { showNotification(`Error: ${result.message}`, false); } } catch (error) { showNotification(`Error: ${error.message}`, false); } } function displayCompareResults(result) { const content = document.getElementById('compareContent'); // Helper function to build flattened destination path // Target: / - /Songs/ function buildFlattenedPath(folderPath, relativePath) { // Extract just the filename from the relative path const pathParts = relativePath.split('/'); const filename = pathParts[pathParts.length - 1]; // Build target: folderPath + Songs/ + filename // Ensure folderPath ends with / const normalizedFolder = folderPath.endsWith('/') ? folderPath : folderPath + '/'; return normalizedFolder + 'Songs/' + filename; } // Determine which folder has the preferred naming format (Artist - Book) const folder1Parts = result.folder1.replace(/\\/g, '/').split('/').filter(p => p); const folder2Parts = result.folder2.replace(/\\/g, '/').split('/').filter(p => p); // Get the last part (book folder name) const folder1Book = folder1Parts[folder1Parts.length - 1] || ''; const folder2Book = folder2Parts[folder2Parts.length - 1] || ''; // Get the second to last part (artist folder name) const folder1Artist = folder1Parts[folder1Parts.length - 2] || ''; const folder2Artist = folder2Parts[folder2Parts.length - 2] || ''; console.log('Folder 1:', folder1Book, 'Artist:', folder1Artist); console.log('Folder 2:', folder2Book, 'Artist:', folder2Artist); // Check if folder name contains " - " (Artist - Book format) const folder1HasDash = folder1Book.includes(' - '); const folder2HasDash = folder2Book.includes(' - '); // Also check if it starts with artist name const folder1StartsWithArtist = folder1Book.toLowerCase().startsWith(folder1Artist.toLowerCase() + ' -'); const folder2StartsWithArtist = folder2Book.toLowerCase().startsWith(folder2Artist.toLowerCase() + ' -'); console.log('F1 has dash:', folder1HasDash, 'starts with artist:', folder1StartsWithArtist); console.log('F2 has dash:', folder2HasDash, 'starts with artist:', folder2StartsWithArtist); let preferredFolder = null; let preferredDirection = null; let preferredName = ''; if ((folder1HasDash || folder1StartsWithArtist) && !folder2HasDash && !folder2StartsWithArtist) { preferredFolder = 1; preferredDirection = 'toF1'; preferredName = folder1Book; } else if ((folder2HasDash || folder2StartsWithArtist) && !folder1HasDash && !folder1StartsWithArtist) { preferredFolder = 2; preferredDirection = 'toF2'; preferredName = folder2Book; } console.log('Preferred folder:', preferredFolder, 'Direction:', preferredDirection); let html = `
0 selected
`; if (preferredFolder) { html += `
🎯 Smart Selection Active: Folder ${preferredFolder} "${preferredName}" has the preferred naming format (Artist - Book). Files are auto-selected to consolidate into this folder. Click "Smart Select" to reapply.
`; } else { html += `
ℹ️ No Preferred Format Detected: Both folders use similar naming. Use "Select All" buttons or manually select files to move.
`; } html += `
${result.stats.identical}
Identical Files
${result.stats.different}
Different Files
${result.stats.only_in_1}
Only in Folder 1
${result.stats.only_in_2}
Only in Folder 2
`; if (result.different.length > 0) { html += '

⚠️ Different Files (Same name, different content)

'; result.different.forEach((file, idx) => { const size1MB = (file.size1 / (1024 * 1024)).toFixed(2); const size2MB = (file.size2 / (1024 * 1024)).toFixed(2); const sizeDiff = ((file.size1 - file.size2) / (1024 * 1024)).toFixed(2); const sizeDiffText = sizeDiff > 0 ? `F1 is ${sizeDiff}MB larger` : `F2 is ${Math.abs(sizeDiff)}MB larger`; const sourcePath1 = result.folder1 + file.path; const sourcePath2 = result.folder2 + file.path; const destPath1 = buildFlattenedPath(result.folder1, file.path); const destPath2 = buildFlattenedPath(result.folder2, file.path); // Auto-select: move smaller file to preferred folder let autoChecked = ''; if (preferredDirection) { if (preferredDirection === 'toF1' && file.size2 < file.size1) { autoChecked = 'data-auto-select="true"'; } else if (preferredDirection === 'toF2' && file.size1 < file.size2) { autoChecked = 'data-auto-select="true"'; } } html += `
${file.path}
F1: ${size1MB}MB | F2: ${size2MB}MB (${sizeDiffText})
`; }); } if (result.only_in_1.length > 0) { html += '

📁 Only in Folder 1

'; result.only_in_1.forEach((file, idx) => { const sizeMB = (file.size / (1024 * 1024)).toFixed(2); const sourcePath = result.folder1 + file.path; const destPath = buildFlattenedPath(result.folder2, file.path); // Auto-select if F2 is preferred const autoChecked = (preferredDirection === 'toF2') ? 'data-auto-select="true"' : ''; html += `
${file.path} (${sizeMB}MB)
`; }); } if (result.only_in_2.length > 0) { html += '

📁 Only in Folder 2

'; result.only_in_2.forEach((file, idx) => { const sizeMB = (file.size / (1024 * 1024)).toFixed(2); const sourcePath = result.folder2 + file.path; const destPath = buildFlattenedPath(result.folder1, file.path); // Auto-select if F1 is preferred const autoChecked = (preferredDirection === 'toF1') ? 'data-auto-select="true"' : ''; html += `
${file.path} (${sizeMB}MB)
`; }); } if (result.identical.length > 0) { html += `

✅ Identical Files (${result.identical.length} files)

`; html += '
Files are byte-for-byte identical (same size and MD5 hash)
'; } html += '
'; content.innerHTML = html; document.getElementById('compareOverlay').classList.add('active'); document.getElementById('compareResults').classList.add('active'); // Auto-select smart selections on load autoSelectSmart(); } function closeCompareResults() { document.getElementById('compareOverlay').classList.remove('active'); document.getElementById('compareResults').classList.remove('active'); } async function moveFile(sourcePath, destPath, deleteSource, button) { const action = deleteSource ? 'move' : 'copy'; const confirmMessage = deleteSource ? `Move "${sourcePath}" to "${destPath}"?\n\nThis will delete the source file.` : `Copy "${sourcePath}" to "${destPath}"?`; if (!confirm(confirmMessage)) { return; } button.disabled = true; button.textContent = deleteSource ? 'Moving...' : 'Copying...'; try { const response = await fetch('/move', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sourcePath: sourcePath, destPath: destPath, deleteSource: deleteSource }) }); const result = await response.json(); if (result.success) { showNotification(result.message, true); // Remove the file item from the list const fileItem = button.closest('.file-item'); fileItem.style.opacity = '0.5'; fileItem.style.pointerEvents = 'none'; // Suggest re-comparing setTimeout(() => { if (confirm('File ${action}d successfully. Re-compare folders to see updated results?')) { compareSelectedFolders(); } }, 500); } else { showNotification(`Error: ${result.message}`, false); button.disabled = false; button.textContent = deleteSource ? 'Move' : 'Copy'; } } catch (error) { showNotification(`Error: ${error.message}`, false); button.disabled = false; button.textContent = deleteSource ? 'Move' : 'Copy'; } } function toggleSelectAll(category) { const checkboxes = document.querySelectorAll(`.file-item[data-category="${category}"] .file-checkbox`); const allChecked = Array.from(checkboxes).every(cb => cb.checked); checkboxes.forEach(cb => { cb.checked = !allChecked; }); updateBatchButtons(); } function updateBatchButtons() { const checkboxes = document.querySelectorAll('.file-checkbox:checked'); const count = checkboxes.length; document.getElementById('selectedCount').textContent = `${count} selected`; document.getElementById('batchMoveToF2').disabled = count === 0; document.getElementById('batchMoveToF1').disabled = count === 0; } function autoSelectSmart() { // Select all checkboxes marked for auto-selection const autoCheckboxes = document.querySelectorAll('.file-checkbox[data-auto-select="true"]'); autoCheckboxes.forEach(cb => { cb.checked = true; }); updateBatchButtons(); } async function batchMove(direction) { const checkboxes = document.querySelectorAll('.file-checkbox:checked'); if (checkboxes.length === 0) { showNotification('No files selected', false); return; } const operations = []; checkboxes.forEach(cb => { if (direction === 'toF2' && cb.dataset.sourceF1) { operations.push({ sourcePath: cb.dataset.sourceF1, destPath: cb.dataset.destF2, deleteSource: true }); } else if (direction === 'toF1' && cb.dataset.sourceF2) { operations.push({ sourcePath: cb.dataset.sourceF2, destPath: cb.dataset.destF1, deleteSource: true }); } }); if (operations.length === 0) { showNotification('No valid operations for selected files', false); return; } const targetFolder = direction === 'toF2' ? 'Folder 2' : 'Folder 1'; if (!confirm(`Move ${operations.length} file(s) to ${targetFolder}?\n\nThis will delete the source files.`)) { return; } // Disable buttons during operation document.getElementById('batchMoveToF2').disabled = true; document.getElementById('batchMoveToF1').disabled = true; document.getElementById('batchMoveToF2').textContent = 'Moving...'; document.getElementById('batchMoveToF1').textContent = 'Moving...'; showNotification(`Moving ${operations.length} files...`, true); try { const response = await fetch('/batch-move', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ operations: operations }) }); const result = await response.json(); if (result.success) { showNotification(result.message, true); // Remove successfully moved items checkboxes.forEach(cb => { const fileItem = cb.closest('.file-item'); fileItem.style.opacity = '0.5'; fileItem.style.pointerEvents = 'none'; }); // Suggest re-comparing setTimeout(() => { if (confirm('Batch move complete. Re-compare folders to see updated results?')) { compareSelectedFolders(); } }, 1000); } else { showNotification(`Error: ${result.message}`, false); } } catch (error) { showNotification(`Error: ${error.message}`, false); } finally { document.getElementById('batchMoveToF2').textContent = 'Move Selected → F2'; document.getElementById('batchMoveToF1').textContent = 'Move Selected → F1'; } } // Initialize: collapse all on load when DOM is ready document.addEventListener('DOMContentLoaded', function() { collapseAll(); });